input 中的 key

我们先来看一个切换登录方式的例子:

<div v-if="isUser">
    <label>Login with account</label>
    <input type="text" placeholder="Enter your account">
</div>
<div v-else>
    <label>Login with email</label>
    <input type="text" placeholder="Enter your email">
</div>

<button @click="isUser=!isUser">click to toggle</button>

我们会发现,在点击按钮切换登录方式后,输入框中已有的内容没有被清除,这是为什么呢?

引用官方文档的原话:

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。

这里的 input 实际上复用了切换之前的 input。而类似 <input><select><textarea> 这样的表单元素都有一个 internal state 保存着元素的值,在元素复用时,这个值是会得到保留的。

如果我们希望切换的时候不保留这个值呢?我们可以给两个 input 添加不同的 key。因为 Vue 是将 key 作为唯一标识从而来识别复用的元素的,如果两个元素的 key 不同,那么就相当于告诉 Vue “这两个元素是完全独立的,你不能用其中一个来复用另一个”。

接着再来看一个利用 v-for 生成 input 的例子。
假如我们的代码为:

<div id="app">
    <div v-for="(item,index) in array">
        {{item}}: <input type="text"> 
    </div>
</div>
const app = new Vue({
    el:'#app',
    data:{
        array:["A","B","C","D","E"]
    }
})

之后生成的 input 中我们填入字符串作为 internal state。如图:

在没有使用 key 的情况下,我们通过 app.array.splice(2,0,"F") 在 BC 之间插入 F,发现:

和之前一样,因为 Vue 采用的是 就地复用 策略,这意味着 ABCDE 在原地不动的情况下被复用了,CDE 都被重新渲染了一次,但先前的 internal state 仍然保留着。

出于性能考虑,有没有办法可以只移动个别元素,单独渲染要插入的那个新元素呢?有了前面的经验,我们会想到给每个 input 一个 key 值。

首先我们尝试将 index 作为 key,之后进行插入操作,发现:

问题依然存在。这是因为,我们将 index 作为复用的判断依据,相当于告诉 Vue:“只要这两个东西的 index 一样,就进行复用”。插入之前 C 的 index 是 2,插入之后 F 的 index 也是 2,于是 F 复用了 C,同理,DE 也被复用了,并因此重新渲染了一次。

index 是会随着插入删除改变的值,所以它实际上并不适合作为 key。于是我们想:在进行插入或者删除操作的时候,有没有一种值始终不会改变呢?有的,我们可以给每个元素一个单独的 id。但更简单的方法是直接使用 item,即元素本身的值,毕竟这个值对每个元素来说也是独一无二的。

我们将 item 作为 key ,之后进行插入操作,发现:

这回正常了。可以很明显地看到,每个元素都复用了先前的对应元素,这是因为此时 item (即元素值)才是复用的判断依据,相当于告诉 Vue:“只要这两个东西的元素值一样,就进行复用”。例如对于 C 来说,它只会复用与自己的值一样的元素,显然这个元素就是 C 本身。同理,D 复用 D,E 复用 E,CDE 都不需要重新渲染了,只需要后移以方便 F 插入,这时候的性能显然要好很多。

Virtual DOM 的 Diff 算法

下面大致从虚拟DOM的Diff算法实现的角度去解释一下。

vue 和 react的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设:

  • 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  • 同一层级的一组节点,他们可以通过唯一的id进行区分。基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)。

引用 React’s diff algorithm 中的例子:

当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。 比如一下这个情况:

我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:

即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,这样显然很没有效率。
所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

所以 key 的作用主要是为了高效的更新虚拟 DOM。

参考:
https://stackoverflow.com/questions/44077320/what-is-the-use-of-track-by-or-key-in-v-for-in-vue-js
https://juejin.im/post/5aae19aa6fb9a028d4445d1a#comment